﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

// MobiLib, Copyright Mirosław M. Mączka.  All Rights Reserved. This code released under the terms of the Microsoft Public License (MS-PL, http://opensource.org/licenses/ms-pl.html.)

namespace MobiLibrary
{
    public partial class MobiLib
    {
        //==================================================================================================================================
        //
        public class EXTH_header:IMobiHeader
        {
            public bool IsDirty { get; internal set; }
            internal MemoryBlock mb;
            public MemoryBlock MB { get { return mb; } }
            //
            /// <summary>
            /// offset do początku każdego rekordu
            /// </summary>
            internal List<int> offsets = new List<int>();
            //
            public readonly int HEADER_SIZE = 12;
            //
            public readonly int RECORD_HEADER_SIZE = 8;
            public readonly int RECORD_TYPE_OFFSET = 0;
            public readonly int RECORD_LENGTH_OFFSET = 4;
            public readonly int RECORD_DATA_OFFSET = 8;
            //
            public string Identifier { get { return Bytes2String(mb.Buffer, 0, 4); } }
            public int HeaderLength { get { return Bytes2Int(mb.Buffer, 4); } internal set { Int2Bytes(value, mb.Buffer, 4); } }
            public int RecordCount { get { return Bytes2Int(mb.Buffer, 8); } internal set { Int2Bytes(value, mb.Buffer, 8); } }
            //
            public EXTH_header(byte[] buff)
            {
                lock (olock)
                {
                    IsDirty = false;
                    mb.Buffer = new byte[buff.Length]; Array.Copy(buff, 0, mb.Buffer, 0, buff.Length);
                    //
                    mkOffsets();
                }
                //
            }
            /// <summary>
            /// generuje tablicę offsets
            /// </summary>
            internal void mkOffsets()
            {
                offsets.Clear();
                int offset = HEADER_SIZE;
                for (int k = 0; k < RecordCount; k++)
                {
                    offsets.Add(offset);
                    offset += Bytes2Int(mb.Buffer, offset + RECORD_LENGTH_OFFSET);
                }
            }
            /// <summary>
            /// *** Główne źródło struktur EXTH_record, używane przz całą resztę ***
            /// </summary>
            /// <param name="i">numer kolejny rekordu</param>
            /// <returns>strukturę EXTH_record lub pustą strukturę, gdy i poza zakresem</returns>
            [System.Runtime.CompilerServices.IndexerName("EXTH_index")]
            internal EXTH_record this[int i]
            {
                get
                {
                    if (i < 0 || i >= RecordCount) return new EXTH_record();
                    return new EXTH_record(mb.Buffer, offsets[i]);
                }
            }
            //
            /// <summary>
            /// Zwraca tablicę z wszystkimi typami rekordów
            /// </summary>
            /// <returns>tablicę wszytkich typów rekordów</returns>
            internal int[] recordsTypes()
            {
                return (from k in Enumerable.Range(0, RecordCount) select this[k].Type).ToArray<int>();
            }
            //
            /// <summary>
            /// Do wyjścia na zewnątrz - źródło surowych rekordów; używa olock
            /// </summary>
            public IEnumerable<EXTH_record> EnumAll
            {
                get
                {
                    lock (olock)
                    {
                        for (int k = 0; k < RecordCount; k++)
                        {
                            yield return this[k];
                        }
                    }
                }
            }
            //
            /*
            [Obsolete]
            public string[] Strings()
            {
                string [] ss = (from k in Enumerable.Range(0, RecordCount) select this[k].ToString()).ToArray();
                //
                return ss;
            }*/
            //
            //
            //----------------------------------------------------------------------------------------------------------------------------------
            //
            internal byte[] getBytesParameter(int type)
            {
                int[] n = (from k in Enumerable.Range(0, RecordCount) where (this[k].Type == type) select k).ToArray<int>();
                if (0 == n.Count<int>()) return null;
                //
                return this[n[0]].Data;
            }
            //
            internal string getStringParameter(int type)
            {
                int [] n = (from k in Enumerable.Range(0, RecordCount) where (this[k].Type == type) select k).ToArray<int>();
                if (0 == n.Count<int>()) return null;
                //
                
                return this[n[0]].sData;
            }
            //
            internal int? getIntParameter(int type)
            {
                int[] n = (from k in Enumerable.Range(0, RecordCount) where (this[k].Type == type) select k).ToArray<int>();
                if (0 == n.Count<int>()) return null;
                //
                return Bytes2Int(this[n[0]].Data, 0);
            }
            //
            internal short? getShortParameter(int type)
            {
                int[] n = (from k in Enumerable.Range(0, RecordCount) where (this[k].Type == type) select k).ToArray<int>();
                if (0 == n.Count<int>()) return null;
                //
                return Bytes2Short(this[n[0]].Data, 0);
            }
            //
            internal byte? getByteParameter(int type)
            {
                int[] n = (from k in Enumerable.Range(0, RecordCount) where (this[k].Type == type) select k).ToArray<int>();
                if (0 == n.Count<int>()) return null;
                //
                return this[n[0]].Data[0];
            }
            //
            //----------------------------------------------------------------------------------------------------------------------------------
            //
            public bool setBytes(int nRecordNumber, byte[] v)
            {
                if (nRecordNumber < 0 || nRecordNumber >= RecordCount) return false;
                //
                EXTH_record er = this[nRecordNumber];
                Array.Clear(mb.Buffer, offsets[nRecordNumber] + RECORD_HEADER_SIZE, er.DataLength);
                Array.Copy(v, 0, mb.Buffer, offsets[nRecordNumber] + RECORD_HEADER_SIZE, v.Length);
                //
                return true;
            }
            //
            public bool setInt(int nRecordNumber, int v)
            {
                if (nRecordNumber < 0 || nRecordNumber >= RecordCount) return false;
                //
                EXTH_record er = this[nRecordNumber];
                Array.Clear(mb.Buffer, offsets[nRecordNumber] + RECORD_HEADER_SIZE, er.DataLength);
                Int2Bytes(v, mb.Buffer, offsets[nRecordNumber] + RECORD_HEADER_SIZE);
                //
                return true;
            }
            //
            public bool setShort(int nRecordNumber, short v)
            {
                if (nRecordNumber < 0 || nRecordNumber >= RecordCount) return false;
                //
                EXTH_record er = this[nRecordNumber];
                Array.Clear(mb.Buffer, offsets[nRecordNumber] + RECORD_HEADER_SIZE, er.DataLength);
                Short2Bytes(v, mb.Buffer, offsets[nRecordNumber] + RECORD_HEADER_SIZE);
                //
                return true;
            }
            //
            public bool setByte(int nRecordNumber, byte v)
            {
                if (nRecordNumber < 0 || nRecordNumber >= RecordCount) return false;
                //
                EXTH_record er = this[nRecordNumber];
                Array.Clear(mb.Buffer, offsets[nRecordNumber] + RECORD_HEADER_SIZE, er.DataLength);
                mb.Buffer[offsets[nRecordNumber] + RECORD_HEADER_SIZE] = v;
                //
                return true;
            }
            //
            //----------------------------------------------------------------------------------------------------------------------------------
            //
            /*
            internal int getRangeSize(int fromRecord, int numberOfRecords)
            {
                if ((fromRecord + numberOfRecords) >= RecordCount) return 0;
                if (0 == numberOfRecords) return 0;
                //
                int n = (from k in Enumerable.Range(fromRecord, numberOfRecords) select (this[k].Length)).Sum();
                //
                return n;
            }
             */
            //
            internal bool deleteAt(int nRecordNumber)
            {
                if (nRecordNumber < 0 || nRecordNumber >= RecordCount) return false;
                //
                //
                if (nRecordNumber == (RecordCount - 1))//ostatni
                {
                    Array.Clear(mb.Buffer, offsets[nRecordNumber], this[nRecordNumber].Length);
                    offsets.RemoveAt(nRecordNumber);
                    RecordCount = RecordCount - 1;
                    return true;
                }
                //
                int rmOffset = offsets[nRecordNumber];
                int rmLength = this[nRecordNumber].Length;
                //
                int mvLength = mb.Length - offsets[nRecordNumber + 1];
                //
                Array.Copy(mb.Buffer, offsets[nRecordNumber + 1], mb.Buffer, rmOffset, mvLength);
                for (int i = nRecordNumber+1; i < RecordCount; i++) offsets[i] -= rmLength;
                offsets.RemoveAt(nRecordNumber);
                RecordCount = RecordCount - 1;
                //
                Array.Clear(mb.Buffer, (offsets[RecordCount - 1] + this[RecordCount - 1].Length), mb.Length - (offsets[RecordCount - 1] + this[RecordCount - 1].Length));
                //
                return true;
            }
            //
            internal bool deleteType(int type)
            {
                //int n = (from k in Enumerable.Range(0, RecordCount) where (type == (new EXTH_record(MB.Buffer, offsets[k]).Type)) select (deleteAt(k) ? 1 : 0)).Sum();
                // UWAGA: deleteAt zmienia RecordCount, ale to nie zmienia RecordCount w Enumerable.Range(0, RecordCount)
                int n = 0;
                for (int k = 0; k < RecordCount; k++)
                {
                    if (type == this[k].Type)
                    {
                        deleteAt(k); n++;// rekord nie powinien wystąpić więcej niż jeden raz, ale to się zdarza...
                    }
                }
                return n>0;
            }
            //
            //
            internal int findType(int type)
            {
                for (int k = 0; k < RecordCount; k++)
                {
                    if (type == this[k].Type) return k;
                }
                return -1;
            }
            //
            /// <summary>
            /// dodatnia ilość brakujących bajtów, lub ujemna - gdy nowa dana krótsza od dotychczasowej
            /// </summary>
            /// <param name="nRecordNumber"></param>
            /// <param name="length"></param>
            /// <returns></returns>
            internal int needBytes(int nRecordNumber, int dataLength)
            {
                return dataLength - (new EXTH_record(mb.Buffer, offsets[nRecordNumber]).DataLength);
                //return i < 0 ? -i : 0;
            }
            //
            internal int freeBytes()
            {
                if (0 == RecordCount) return mb.Length - HEADER_SIZE;
                return (mb.Length - HEADER_SIZE) - (offsets[RecordCount-1] + this[RecordCount-1].Length);
            }
            //
            internal bool expandRecordData(int nRecordNumber, int newDataLengthIncrease)
            {
                // założenie: obszar EXTHDR został już powiększony
                // przesunąć od następnego rekordu dalej, uaktualnić offsets
                //
                if(nRecordNumber== (RecordCount-1))//ostatni, tylko zmiana wielkości
                {
                    Int2Bytes(this[RecordCount - 1].Length + newDataLengthIncrease, mb.Buffer, offsets[nRecordNumber] + RECORD_LENGTH_OFFSET);
                    return true;
                }
                //
                int newLeght = this[nRecordNumber].Length + newDataLengthIncrease;
                int dataLenthToMove = 0;
                for (int i = nRecordNumber + 1; i < RecordCount;i++ )
                {
                    dataLenthToMove += this[i].Length;
                }
                //
                // czy to jest prostsze?
                //int n = (from k in Enumerable.Range(nRecordNumber + 1, RecordCount - 1 - nRecordNumber) select (new EXTH_record(mb.Buffer, offsets[k]).Length)).Sum();
                //
                int beginDataToMove = offsets[nRecordNumber+1];
                Array.Copy(mb.Buffer, beginDataToMove, mb.Buffer, beginDataToMove + newDataLengthIncrease, dataLenthToMove);
                Int2Bytes(newLeght, mb.Buffer, offsets[nRecordNumber] + RECORD_LENGTH_OFFSET);
                //
                mkOffsets();
                //
                return true;
            }
            //
            internal bool contractRecordData(int nRecordNumber, int newDataLengthDecrease)
            {
                //
                // przesunąć bliżej następny rekord dalej, uaktualnić offsets
                if (nRecordNumber == (RecordCount - 1))//ostatni, tylko zmiana wielkości
                {
                    Int2Bytes(this[RecordCount - 1].Length + newDataLengthDecrease, mb.Buffer, offsets[nRecordNumber] + RECORD_LENGTH_OFFSET);
                    Array.Clear(mb.Buffer, offsets[nRecordNumber] + this[nRecordNumber].Length + newDataLengthDecrease, Math.Abs(newDataLengthDecrease));
                    //
                    return true;
                }
                //
                int newLeght = this[nRecordNumber].Length + newDataLengthDecrease;
                int dataLenthToMove = 0;
                for (int i = nRecordNumber + 1; i < RecordCount; i++)
                {
                    dataLenthToMove += this[i].Length;
                }
                //
                int beginDataToMove = offsets[nRecordNumber + 1];
                Array.Copy(mb.Buffer, beginDataToMove, mb.Buffer, beginDataToMove + newDataLengthDecrease, dataLenthToMove);
                Int2Bytes(newLeght, mb.Buffer, offsets[nRecordNumber] + RECORD_LENGTH_OFFSET);
                //
                Array.Clear(mb.Buffer, beginDataToMove + dataLenthToMove + newDataLengthDecrease, Math.Abs(newDataLengthDecrease));
                //
                mkOffsets();
                //
                return true;
            }
            //
            /// <summary>
            /// 
            /// </summary>
            /// <param name="type"></param>
            /// <param name="newDataLength"></param>
            /// <returns></returns>
            internal int addRecord(int type, int newDataLength)
            {
                int beginNew = (0==RecordCount)?HEADER_SIZE:offsets[RecordCount-1] + this[RecordCount-1].Length;
                //
                Int2Bytes(type, mb.Buffer, beginNew+RECORD_TYPE_OFFSET);//type
                Int2Bytes(newDataLength + RECORD_HEADER_SIZE, mb.Buffer, beginNew + RECORD_LENGTH_OFFSET);//length
                Array.Clear(mb.Buffer, beginNew + RECORD_HEADER_SIZE, newDataLength);
                offsets.Add(beginNew);
                RecordCount = RecordCount + 1;
                return RecordCount - 1;
            }
            //
            //----------------------------------------------------------------------------------------------------------------------------------
            //
            internal bool deleteDoubles()
            {
                bool isChanged = false;
                //
                System.Collections.Hashtable ht = new System.Collections.Hashtable();
                for(int i=0; i< RecordCount; i++)
                {
                    int type = this[i].Type;
                    if (ht.Contains(type))
                    {
                        ((List<int>)ht[type]).Add(i);
                    }
                    else
                    {
                        ht.Add(type, (object)(new List<int>()));
                        ((List<int>)ht[type]).Add(i);
                    }
                }
                //
                List<int> l = new List<int>();
                foreach (int type in ht.Keys)
                {
                    if (((List<int>)ht[type]).Count > 1)
                    {
                        bool first=true;
                        foreach (int n in ((List<int>)ht[type]))
                        {
                            if (first)
                            {
                                first = false; continue;
                            }
                            l.Add(n);
                        }
                    }
                }
                //
                // kasowanie od tyłu, by usunięcie rekordu nie zmieniało numeracji poprzednivch
                l.Sort();
                l.Reverse();
                foreach (int n in l)
                {
                    deleteAt(n); isChanged = true;
                }
                //
                return isChanged;
            }
            //
            //----------------------------------------------------------------------------------------------------------------------------------
            //
            internal void Store(MemoryBlock MetaBuffer)
            {
                lock (olock)
                {
                    Array.Copy(mb.Buffer, 0, MetaBuffer.Buffer, mb.Offset, mb.Length);
                }
            }
            //
            //
            public override string ToString()
            {
                lock (olock)
                {
                    //string s = "EXTH_header: MB=" + MB.ToString() + "\n";
                    //s += string.Format("Identifier={0},HeaderLength={1},RecordCount={2}\n", Identifier, HeaderLength, RecordCount);
                    //
                    string s = string.Format("EXTH_header({0}):\n", RecordCount);
                    //
                    for (int i = 0; i < RecordCount; i++)
                    {
                        //s += string.Format("  {0}) Type={1},Length={2},sData=[{3}]\n", i, CETHX.ToText( this[i].Type, this[i].Length, this[i].sData);
                        s += string.Format("  {0}) {1}\n", i, CETHX.ToText(this[i].Type, this[i].Length, this[i].Data));
                    }
                    return s;
                }
            }
            //
            public string Dump()
            {
                lock (olock)
                {
                    //string s = "EXTH_header: MB=" + MB.ToString() + "\n";
                    //s += string.Format("Identifier={0},HeaderLength={1},RecordCount={2}\n", Identifier, HeaderLength, RecordCount);
                    //
                    string s = string.Format("EXTH_header({0}):\n", RecordCount);
                    //
                    for (int i = 0; i < RecordCount; i++)
                    {
                        //s += string.Format("  {0}) Type={1},Length={2},sData=[{3}]\n", i, CETHX.ToText( this[i].Type, this[i].Length, this[i].sData);
                        s += string.Format("  {0}) {1}\n", i, CETHX.ToText(this[i].Type, this[i].Length, this[i].Data));
                    }
                    return s;
                }
            }
            //----------------------------------------------------------------------------------------------------------------------------------
            //
            private bool Expand(MemoryBlock MetaBuffer, MobiLib ml, int newSize)
            {
                if (mb.Length >= newSize) return true;
                //
                if ((mb.Offset + ml.mfn.mb.Length + newSize + 32) > MetaBuffer.Length) return false;
                //
                byte[] bb = new byte[newSize];
                // trzeba skopiować także FileName
                Array.Copy(mb.Buffer, bb, mb.Length);
                mb.Buffer = bb;
                HeaderLength = mb.Length;
                //
                //
                return true;
            }
            //----------------------------------------------------------------------------------------------------------------------------------
        }//public class EXTH_header
        //
        //----------------------------------------------------------------------------------------------------------------------------------
        //----------------------------------------------------------------------------------------------------------------------------------
        //----------------------------------------------------------------------------------------------------------------------------------

        //
        //
        //==================================================================================================================================
        //
        public struct EXTH_record
        {
            public int Type { get; set; }
            public int Length { get; set; }
            public int DataLength { get; set; }
            public byte[] Data { get; set; }
            public string sData { get; set; }
            //
            public EXTH_record(Byte[] b, int offset)
                : this()
            {
                Type = Bytes2Int(b, offset);
                Length = Bytes2Int(b, offset + 4);
                Data = new byte[Length - 8]; Array.Copy(b, offset + 8, Data, 0, Length - 8);
                DataLength = Length - 8;
                sData = CETHX.Data2String(Type, Length, Data);
                //sData = Bytes2String(b, offset + 8, Length - 8);
            }
            public override string ToString()
            {
                return string.Format("{0}", CETHX.ToText(Type, Length, Data));
            }
        }
        //==================================================================================================================================
        //
        public struct exthRecordAsText
        {
            public int n { get; internal set; }
            public int Type { get; internal set; }
            public string sData { get; internal set; }
            //
            public string TypeName { get { return CETHX.Type2Name(Type); } }
            public string OneLine { get { return TypeName + ": " + sData; } }
            //
            public exthRecordAsText(int n, EXTH_record r)
                : this()
            {
                this.n = n;
                Type = r.Type;
                sData = r.sData;
            }
        }
        //
        //==================================================================================================================================
    }//class
}//namespace
